const mongoose = require('mongoose');
const InvokeRelation = require('./mongo/invoke-relation');
const Function = require('./mongo/function');
const FunctionVariable = require('./mongo/function-variable');
const TestCase = require('./mongo/test-case');
const functions = require('./functions');
const fileElements = require('./file-elements');
const functionRelations = require('./function-relations');
const utility = require('./utility');
const numbers = require('./numbers');
const { eventHub, eventKind } = require('./event-hub');
const File = require('./mongo/file');
const TestInvoke = require('./mongo/test-invoke');
const ProjectVersion = require('./mongo/project-version');
const logger = require('./logger');

const defaultStubDepth = 3;

/**
 * 调用关系数据格式化
 * @param {String} versionId 版本Id
 * @param {String} functionId 函数Id
 * @param {Object} relation 调用关系数据
 * @returns {Object}
 */
function formatRelation(versionId, functionId, relation) {
  return {
    versionId: mongoose.Types.ObjectId(versionId),
    functionId: mongoose.Types.ObjectId(functionId),
    fromFuncId: mongoose.Types.ObjectId(relation.fromFuncId),
    fromFuncMangledId: relation.fromFuncMangledId,
    fromFuncFileId: mongoose.Types.ObjectId(relation.fromFuncFileId),
    toFuncId: relation.toFuncId && mongoose.Types.ObjectId(relation.toFuncId),
    toFuncName: relation.toFuncName,
    toFuncMangledId: relation.toFuncMangledId,
    toFuncFileId: mongoose.Types.ObjectId(relation.toFuncFileId),
    toFuncPointer: relation.toFuncPointer || false,
    enableStub: !relation.toFuncId,
  };
}

async function getInvokeCount(functionId) {
  const count = await InvokeRelation.countDocuments({
    functionId: mongoose.Types.ObjectId(functionId),
  });
  return count;
}

/**
 * 删除函数调用关系
 * @param {Array<String>} functionIds 函数Id数组
 * @param {String} versionId
 * @returns {Promise}
 */
async function removeRelationsOfFunctions(functionIds, versionId) {
  if (!utility.isArrayType(functionIds)) {
    functionIds = [functionIds];
  }
  functionIds = functionIds.map((id) => mongoose.Types.ObjectId(id));
  await InvokeRelation.deleteMany({ functionId: { $in: functionIds } });
  // eslint-disable-next-line no-use-before-define
  const invokedFuncs = (await getInvokeFunctions(functionIds, versionId)).map(
    (id) => mongoose.Types.ObjectId(id)
  );
  await functionRelations.removeRelationsOfFunctions(functionIds);
  if (invokedFuncs.length === 0) {
    return;
  }
  const funcsWithTestcase = (await Function.find({ _id: { $in: invokedFuncs }, testCaseCreate: true }, ['_id']))
    .map((func) => func._id);
  await Promise.all([
    InvokeRelation.updateMany(
      {
        toFuncId: { $in: functionIds },
        functionId: { $nin: funcsWithTestcase },
      },
      { $set: { toFuncId: null } }
    ),
    InvokeRelation.updateMany(
      {
        toFuncId: { $in: functionIds },
        functionId: { $in: funcsWithTestcase },
      },
      { $set: { toFuncId: null, toFuncFileUpdate: true } }
    ),
  ]);
  // eslint-disable-next-line no-use-before-define
  await functions.updateInvokeCoverageTotal(invokedFuncs, versionId);
}

/**
 * 删除版本的函数调用关系
 * @param {Array<String>} versionIds 版本Id数组
 * @returns {Promise}
 */
async function removeRelationsOfVersions(versionIds) {
  if (!utility.isArrayType(versionIds)) {
    versionIds = [versionIds];
  }
  versionIds = versionIds.map((id) => mongoose.Types.ObjectId(id));
  await Promise.all([
    InvokeRelation.deleteMany({ versionId: { $in: versionIds } }),
    functionRelations.removeRelationsOfVersions(versionIds),
  ]);
}
/**
 * 更新父函数的私有调用关系
 * @param {String} versionId 版本Id
 * @param {String} fileId 调用函数文件Id
 * @param {String} functionId 调用函数Id
 * @param {String} mangledId 调用函数mangledId
 * @param {Array<Object>} records 调用关系
 * @returns {Promise}
 */
async function updateParents(versionId, fileId, functionId, mangledId, records) {
  functionId = mongoose.Types.ObjectId(functionId);
  const invokeParents = await InvokeRelation.aggregate([
    {
      $match: {
        versionId: mongoose.Types.ObjectId(versionId),
        toFuncMangledId: mangledId,
        $or: [{ toFuncId: null }, { toFuncId: functionId }],
      },
    },
    {
      $group: {
        _id: '$functionId',
      },
    },
    {
      $lookup: {
        from: 'functions', localField: '_id', foreignField: '_id', as: 'functions',
      },
    },
    {
      $project: {
        functionId: '$_id',
        testCaseCreate: '$functions.testCaseCreate',
      },
    },
  ]);
  if (invokeParents.length === 0) {
    return;
  }
  const insertRelations = [];
  await InvokeRelation.bulkWrite(invokeParents.map((parent) => {
    const doc = { $set: { toFuncId: functionId, toFuncFileId: fileId, toFuncFileUpdate: true } };
    if (parent.testCaseCreate.length === 0 || !parent.testCaseCreate[0]) {
      doc.$set.enableStub = false;
      doc.$set.toFuncFileUpdate = false;
    }
    records.forEach((record) => {
      record.functionId = parent.functionId;
      insertRelations.push({ ...record });
    });
    return {
      updateMany: {
        filter: { functionId: parent.functionId, toFuncMangledId: mangledId, toFuncId: null },
        update: doc,
      },
    };
  }));
  await Promise.all(utility.arrayChunk(insertRelations, 50000).map(
    (relations) => InvokeRelation.collection.insertMany(relations, { ordered: false })
      .catch(() => { })
  ));
}

/**
 * 为入口函数创建调用关系
 * @param {String} versionId 版本Id
 * @param {String} functionId 调用函数Id
 * @param {String} mangledId 调用函数MangledId
 * @param {String} fileId 入口函数文件Id
 * @returns {Promise}
 */
async function createRelations(versionId, functionId, mangledId, fileId) {
  let relations = await functionRelations.retrieveRelations(functionId, versionId);
  relations = relations.map((relation) => formatRelation(versionId, functionId, relation));
  if (relations.length > 0 && (await getInvokeCount(functionId))) {
    await InvokeRelation.collection.insertMany(
      relations,
      { ordered: false }
    ).catch(() => { });
  }
  // eslint-disable-next-line no-use-before-define
  await updateParents(versionId, fileId, functionId, mangledId, relations);
}

async function createRelationsForConstructor(versionId, functionId, mangledId, functionName) {
  const fromFunc = await functions.getFunction(functionId);
  const func = await functions.getFunctionByMangledId(versionId, mangledId);
  const relations = [{
    versionId: fromFunc.versionId,
    functionId: fromFunc._id,
    fromFuncId: fromFunc._id,
    fromFuncMangledId: fromFunc.mangledId,
    fromFuncFileId: fromFunc.fileId,
    toFuncId: func.length > 0 ? func[0]._id : null,
    toFuncName: func.length > 0 ? func[0].functionFullName : functionName,
    toFuncMangledId: mangledId,
    toFuncFileId: func.length > 0 ? func[0].fileId : fromFunc.fileId,
    toFuncPointer: false,
    toFuncCtor: true,
    enableStub: true,
  }];
  if (func.length > 0) {
    const subRelations = await functionRelations.retrieveRelations(func[0]._id, versionId);
    subRelations.forEach((relation) => relations.push(formatRelation(versionId, functionId, relation)));
  }
  await InvokeRelation.collection.insertMany(
    relations,
    { ordered: false }
  ).catch(() => { });
}

/**
 * 删除断开的无用调用关系，排序
 * @param {String} functionId 函数Id
 * @param {Array<Object>} records 调用关系
 * @param {Object} options
 * @returns {Promise}
 */
async function filterRelations(functionId, relations, { sort = false, completeDepth = false, includeCtor = true } = {}) {
  const relationMap = new Map();
  relations.forEach((relation) => {
    const fromFuncId = relation.fromFuncId.toString();
    if (!relationMap.has(fromFuncId)) {
      relationMap.set(fromFuncId, new Map());
    }
    relationMap.get(fromFuncId).set(
      relation.toFuncId ? relation.toFuncId.toString() : relation.toFuncMangledId,
      relation,
    );
  });
  const invokedFuncs = new Set();
  const displayFuncs = new Set();
  const calledChain = new Set();
  const depthsMap = new Map();

  const queue = [{
    functionId: functionId.toString(),
    stub: false,
    toFuncCtor: false,
    funcFileUpdate: false,
    depth: 1,
  }];

  // 改为广度遍历算法，便于层级的计算
  let cursor = 0;
  while (cursor < queue.length) {
    const {
      functionId,
      toFuncCtor,
      funcFileUpdate,
    } = queue[cursor];
    let { stub } = queue[cursor];
    let { depth } = queue[cursor];
    cursor += 1;
    invokedFuncs.add(functionId);
    if (!includeCtor && toFuncCtor) {
      stub = true;
    }
    if (!stub && !funcFileUpdate) {
      displayFuncs.add(functionId);
    }
    if (!relationMap.has(functionId)) {
      // eslint-disable-next-line no-continue
      continue;
    }
    depth += 1;
    relationMap.get(functionId).forEach((childRelations) => {
      const {
        toFuncId,
        toFuncMangledId,
        enableStub,
        toFuncCtor,
        toFuncFileUpdate,
        toFuncFileId,
      } = childRelations;
      let key = `${functionId}:${toFuncId}`;
      if (!toFuncId) {
        key = `${functionId}:${toFuncMangledId}`;
      }
      if (!stub) {
        if (!toFuncId) {
          if (!depthsMap.has(`${toFuncMangledId}:${toFuncFileId}`)) {
            depthsMap.set(`${toFuncMangledId}:${toFuncFileId}`, { minDepth: depth, maxDepth: depth, depths: new Set([depth]) });
          }
          depthsMap.get(`${toFuncMangledId}:${toFuncFileId}`).depths.add(depth);
          depthsMap.get(`${toFuncMangledId}:${toFuncFileId}`).maxDepth = Math.max(depthsMap.get(`${toFuncMangledId}:${toFuncFileId}`).maxDepth, depth);
        } else {
          if (!depthsMap.has(toFuncId.toString())) {
            depthsMap.set(toFuncId.toString(), { minDepth: depth, maxDepth: depth, depths: new Set([depth]) });
          }
          depthsMap.get(toFuncId.toString()).depths.add(depth);
          depthsMap.get(toFuncId.toString()).maxDepth = Math.max(depthsMap.get(toFuncId.toString()).maxDepth, depth);
        }
      }
      if (calledChain.has(key)) {
        return;
      }
      calledChain.add(key);
      if (!toFuncId) {
        return;
      }
      queue.push({
        functionId: toFuncId.toString(),
        stub: stub ? true : Number(enableStub),
        toFuncCtor: Number(toFuncCtor),
        funcFileUpdate: funcFileUpdate ? true : Number(toFuncFileUpdate),
        depth,
      });
    });
  }
  calledChain.clear();
  calledChain.add(functionId.toString());
  if (completeDepth) {
    queue.forEach(({ functionId, stub }) => {
      if (calledChain.has(functionId) || !relationMap.has(functionId) || stub) {
        return;
      }
      calledChain.add(functionId);
      let parentDepths = null;
      if (depthsMap.has(functionId)) {
        parentDepths = depthsMap.get(functionId).depths;
      }
      relationMap.get(functionId).forEach((childRelations) => {
        const {
          toFuncId,
          toFuncMangledId,
          toFuncFileId,
        } = childRelations;
        let depthKey = '';
        if (!toFuncId) {
          depthKey = `${toFuncMangledId}:${toFuncFileId}`;
        } else {
          depthKey = toFuncId.toString();
        }
        if (calledChain.has(depthKey)) {
          return;
        }
        if (parentDepths && depthsMap.has(depthKey)) {
          parentDepths.forEach((depth) => {
            depthsMap.get(depthKey).depths.add(depth + 1);
          });
        }
      });
    });
    depthsMap.forEach((depth) => {
      let { maxDepth } = depth;
      depth.depths.forEach((d) => {
        if (d > maxDepth) {
          maxDepth = d;
        }
      });
      depth.maxDepth = maxDepth;
    });
  }
  const deadRelations = new Set();
  relations = relations.filter((relation) => {
    const key = relation.toFuncId ? relation.toFuncId.toString() : `${relation.toFuncMangledId}:${relation.toFuncFileId}`;
    if (depthsMap.has(key)) {
      relation.depth = depthsMap.get(key);
    }
    const fromFuncId = relation.fromFuncId.toString();
    if (displayFuncs.has(fromFuncId)) {
      return true;
    }
    if (!invokedFuncs.has(fromFuncId)) {
      deadRelations.add(relation._id.toString());
    }
    return false;
  });
  InvokeRelation.deleteMany(
    { _id: { $in: [...deadRelations].map((id) => mongoose.Types.ObjectId(id)) } }
  )
    .then(() => deadRelations.clear());
  invokedFuncs.clear();
  displayFuncs.clear();
  if (sort) {
    return relations.sort((a, b) => {
      const [prevLevel, nextLevel] = [a, b].map((relation) => {
        const key = relation.toFuncId ? relation.toFuncId.toString() : `${relation.toFuncMangledId}:${relation.toFuncFileId}`;
        return depthsMap.has(key) ? depthsMap.get(key).minDepth : 0;
      });
      if (prevLevel < nextLevel) return -1;
      if (prevLevel > nextLevel) return 1;
      return 0;
    });
  }
  return relations;
}

function stubFuncsByDefaultDepth(functionId, relations, specFuncIds, defaultDepth = defaultStubDepth) {
  const relationMap = new Map();
  const depthsMap = new Map();
  relations.forEach((relation) => {
    relation.enableStub = relation.enableStub !== undefined ? relation.enableStub : !relation.toFuncId;
    const fromFuncId = relation.fromFuncId.toString();
    if (!relationMap.has(fromFuncId)) {
      relationMap.set(fromFuncId, new Map());
    }
    if (!relation.toFuncId) {
      return;
    }
    relationMap.get(fromFuncId).set(relation.toFuncId.toString(), relation);
  });
  const calledChain = new Set();
  const queue = [{
    functionId: functionId.toString(),
    depth: 1,
  }];

  let cursor = 0;
  while (cursor < queue.length) {
    const {
      functionId,
    } = queue[cursor];
    let { depth } = queue[cursor];
    cursor += 1;
    if (!relationMap.has(functionId)) {
      // eslint-disable-next-line no-continue
      continue;
    }
    depth += 1;
    relationMap.get(functionId).forEach(({ toFuncId }) => {
      if (!toFuncId) {
        return;
      }
      if (!depthsMap.has(toFuncId.toString())) {
        depthsMap.set(toFuncId.toString(), new Set([depth]));
      }
      depthsMap.get(toFuncId.toString()).add(depth);
      const key = `${functionId}:${toFuncId}`;
      if (calledChain.has(key)) {
        return;
      }
      calledChain.add(key);
      queue.push({
        functionId: toFuncId.toString(),
        depth,
      });
    });
  }
  calledChain.clear();
  calledChain.add(functionId.toString());
  queue.forEach(({ functionId }) => {
    if (calledChain.has(functionId) || !relationMap.has(functionId)) {
      return;
    }
    calledChain.add(functionId);
    let parentDepths = null;
    if (depthsMap.has(functionId)) {
      parentDepths = depthsMap.get(functionId);
    }
    relationMap.get(functionId).forEach((childRelations) => {
      const {
        toFuncId,
      } = childRelations;
      if (!toFuncId) {
        return;
      }
      const depthKey = toFuncId.toString();
      if (calledChain.has(depthKey)) {
        return;
      }
      if (parentDepths && depthsMap.has(depthKey)) {
        parentDepths.forEach((depth) => {
          depthsMap.get(depthKey).add(depth + 1);
        });
      }
    });
  });
  relations.forEach((relation) => {
    if (relation.toFuncId
      && !specFuncIds.has(relation.toFuncId.toString())
      && depthsMap.has(relation.toFuncId.toString())
      && depthsMap.get(relation.toFuncId.toString()).has(defaultDepth)
    ) {
      relation.enableStub = true;
    }
  });
  return relations;
}

async function generateRelations(functionId) {
  try {
    const waited = await numbers.waitLock(
      numbers.getInvokeGenLockKey(functionId),
      { expireSec: 60, retryInterval: 100 }
    );
    if (waited) {
      return;
    }
    functionId = mongoose.Types.ObjectId(functionId);
    const func = await Function.findOne({ _id: functionId });
    if (!func) {
      return;
    }
    let relations = await functionRelations.retrieveRelations(functionId, func.versionId);
    const version = await ProjectVersion.findOne({ _id: func.versionId });
    const funcs = await functions.getFunctions(func.versionId, { kinds: [fileElements.elementKind.specialFunction], projection: { _id: 1 } });
    const specFuncIds = new Set();
    funcs.forEach((func) => {
      specFuncIds.add(func._id.toString());
    });
    relations = stubFuncsByDefaultDepth(functionId, relations, specFuncIds, version.stubDepth);
    await InvokeRelation.collection.insertMany(relations.map((relation) => {
      delete relation._id;
      relation.functionId = functionId;
      relation.enableStub = relation.enableStub !== undefined ? relation.enableStub : !relation.toFuncId;
      relation.toFuncFileUpdate = false;
      return relation;
    }), { ordered: false }).catch(() => { });
  } finally {
    await numbers.releaseLock(numbers.getInvokeGenLockKey(functionId));
  }
}

/**
 * 获取某个函数下的调用关系
 * @param {String} functionId 函数Id
 * @param {Object} options
 * @returns {Promise}
 */
async function retrieveRelations(
  functionId,
  {
    sort = false,
    initRelations = false,
    completeDepth = false,
    includeCtor = true,
  } = {}
) {
  let relations = await InvokeRelation.find({ functionId: mongoose.Types.ObjectId(functionId) }).lean();
  if (relations.length === 0 && initRelations) {
    await generateRelations(functionId);
    relations = await InvokeRelation.find({ functionId: mongoose.Types.ObjectId(functionId) }).lean();
  }
  return filterRelations(
    functionId.toString(),
    relations,
    { sort, completeDepth, includeCtor }
  );
}

/**
 * 删除两个函数间的调用关系
 * @param {String} fromFuncId 调用函数Id
 * @param {String} toFuncMangledId 被调函数Id
 * @param {String} versionId 版本Id
 * @param {Boolean} updateCoverage 是否更新覆盖率分母
 * @returns {Promise}
 */
async function removeRelation(
  fromFuncId,
  toFuncMangledId,
  versionId = null,
  updateCoverage = true
) {
  fromFuncId = mongoose.Types.ObjectId(fromFuncId);
  if (!versionId) {
    const func = await Function.findOne({ _id: fromFuncId });
    if (!func) {
      return;
    }
    versionId = func.versionId;
  } else {
    versionId = mongoose.Types.ObjectId(versionId);
  }
  await functionRelations.removeRelations(fromFuncId, toFuncMangledId, versionId);
  const filter = {
    versionId,
    toFuncMangledId,
    fromFuncId,
  };
  const funcs = await InvokeRelation.find(filter, ['functionId']);
  await InvokeRelation.deleteMany(filter);
  if (funcs.length === 0) {
    return;
  }
  if (!updateCoverage) {
    return;
  }
  await functions.updateInvokeCoverageTotal(funcs.map((func) => func.functionId), versionId);
}

/**
 * 调用关系更新打桩状态
 * @param {String} functionId 函数Id
 * @param {String} toFuncId 被调定义函数Id
 * @param {String} mangledId 被调未定义函数mangledId
 * @param {String} functionName 被调未定义函数名
 * @param {Boolean} stubStatus 打桩状态
 * @returns {Promise}
 */
async function updateStubStatus(functionId, toFuncId, mangledId, functionName, stubStatus) {
  functionId = mongoose.Types.ObjectId(functionId);
  const func = await Function.findOne({ _id: functionId });
  if (!func) {
    return;
  }
  let toFunc;
  if (toFuncId) {
    // 定义函数,且当前选择为定义文件
    toFunc = await Function.findOne({ _id: mongoose.Types.ObjectId(toFuncId) });
    if (!toFunc) {
      return;
    }
    if (!mangledId) {
      mangledId = toFuncId.mangledId;
    }
    await InvokeRelation.updateMany(
      {
        functionId: mongoose.Types.ObjectId(functionId),
        toFuncId: mongoose.Types.ObjectId(toFuncId),
      },
      {
        $set: {
          enableStub: stubStatus,
        },
      }
    );
  } else if (mangledId) {
    // 未定义函数，或定义函数当前选择为调用文件
    await InvokeRelation.updateMany(
      {
        functionId: mongoose.Types.ObjectId(functionId),
        toFuncId: null,
        toFuncMangledId: mangledId,
      },
      {
        $set: {
          enableStub: stubStatus,
        },
      }
    );
  }
  if (!stubStatus) {
    const doc = {};
    let stubName;
    if (toFuncId) {
      stubName = functions.isCFunction(toFunc) ? toFunc.functionName : toFunc.mangledId;
    } else {
      // 未定义函数判断c/c++
      const fromFunctionVariable = await InvokeRelation.aggregate([
        {
          $match: {
            functionId: mongoose.Types.ObjectId(functionId),
            toFuncId: null,
            toFuncMangledId: mangledId,
          },
        },
        {
          $lookup: {
            from: 'function-variables', localField: 'fromFuncId', foreignField: 'functionId', as: 'function-variables',
          },
        },
        {
          $unwind: '$function-variables',
        },
        {
          $project: {
            stubs: '$function-variables.stubs',
          },
        },
        { $limit: 1 },
      ]);
      if (fromFunctionVariable.length > 0 && fromFunctionVariable[0].stubs
        && Object.prototype.hasOwnProperty.call(fromFunctionVariable[0].stubs, mangledId)) {
        // c++
        stubName = mangledId;
      } else {
        // c
        stubName = functionName;
      }
    }
    let testCaseStubName = stubName;
    // 集成测试已定义(选择了定义文件)且来自其它文件的桩函数在测试用例中需要@fileId
    if (toFuncId && (func.fileId.toString() !== toFunc.fileId.toString())) {
      testCaseStubName += `@${toFunc.fileId.toString()}`;
    }
    doc[`data.stubs.${testCaseStubName}`] = 1;
    doc[`data.output.stubs.${testCaseStubName}`] = 1;
    await TestCase.updateMany({ functionId }, { $unset: doc });
    // 删除用户自定义的桩函数指针目标
    const stub = await FunctionVariable.findOne({ functionId, [`stubs.${stubName}.@mangled`]: { $exists: 1 } });
    // 入口函数的stubs信息
    if (stub) {
      await FunctionVariable.updateOne({ functionId }, { $unset: { [`stubs.${stubName}.pointerTargets`]: 1 } });
      // 集成函数添加的pointerTargets
    } else {
      await FunctionVariable.updateOne({ functionId }, { $unset: { [`stubs.${stubName}`]: 1 } });
    }
    // 清除桩函数参数检查项
    await FunctionVariable.updateOne({ functionId }, { $unset: { [`output.stubs.${stubName}`]: 1 } });
  } else {
    await Promise.all([
      FunctionVariable.updateOne(
        { functionId },
        {
          $unset:
          {
            [`${functions.integratedFuncKey}.${mangledId}`]: 1,
            [`output.${functions.integratedFuncKey}.${mangledId}`]: 1,
          },
        }
      ),
      TestCase.updateMany(
        { functionId },
        {
          $unset:
          {
            [`data.${functions.integratedFuncKey}.${mangledId}`]: 1,
            [`data.output.${functions.integratedFuncKey}.${mangledId}`]: 1,
          },
        }
      ),
    ]);
  }
}

/**
 * 调用关系更新所属文件
 * @param {String} functionId 函数Id
 * @param {String} toFuncMangledId 被调函数MangledId
 * @param {String} originalFileId 调用原文件
 * @param {String} fileId 调用新文件
 * @param {String} versionId 版本Id
 * @returns {Promise}
 */
async function updateInvokeFile(functionId, toFuncMangledId, originalFileId, fileId, versionId) {
  functionId = mongoose.Types.ObjectId(functionId);
  const relations = await InvokeRelation.find(
    {
      functionId,
      toFuncMangledId,
    }
  );
  if (!relations) {
    return;
  }
  const originalRelations = [];
  relations.forEach((relation) => {
    if (relation.toFuncFileId.toString() === originalFileId) {
      originalRelations.push(relation);
    }
  });
  if (originalRelations.length === 0) {
    return;
  }
  const relationIds = originalRelations.map((originalRelation) => originalRelation._id);
  const toFuncFileId = mongoose.Types.ObjectId(fileId);
  const newFunc = await Function.findOne({ fileId: toFuncFileId, mangledId: toFuncMangledId });
  let toFuncId = null;
  // 已定义函数选择调用文件 toFuncId: null
  if (newFunc) {
    toFuncId = newFunc._id;
  }
  const doc = { $set: { toFuncId, toFuncFileId, toFuncFileUpdate: false } };

  await InvokeRelation.updateMany({ _id: { $in: relationIds } }, doc);
  if (toFuncId === null) {
    await functions.updateInvokeCoverageTotal([functionId], versionId);
    return;
  }
  const functionVariables = await FunctionVariable.findOne({ functionId: newFunc._id });
  if (!functionVariables) {
    return;
  }
  // 已定义函数选择定义文件则检查有无新加入的调用关系
  const newRelations = await functionRelations.retrieveRelations(newFunc._id, versionId);
  if (newRelations.length > 0) {
    await InvokeRelation.collection.insertMany(
      newRelations.map((relation) => formatRelation(versionId, functionId, relation)),
      { ordered: false }
    ).catch(() => { });
  }
  await functions.updateInvokeCoverageTotal([functionId], versionId);
}

/**
 * 为手动创建的桩函数添加调用关系
 * @param {String} functionId 函数Id
 * @param {String} toFuncMangledId 被调函数MangledId
 * @param {String} toFuncName 被调函数名
 * @returns {Promise}
 */
async function createRelationsForManualStub(functionId, toFuncMangledId, toFuncName) {
  functionId = mongoose.Types.ObjectId(functionId);
  const func = await Function.findOne(functionId);
  const relations = [{
    versionId: func.versionId,
    functionId,
    fromFuncId: functionId,
    fromFuncMangledId: func.mangledId,
    fromFuncFileId: func.fileId,
    toFuncId: null,
    toFuncName,
    toFuncMangledId,
    toFuncFileId: func.fileId,
    toFuncPointer: true,
    enableStub: true,
  }];
  await InvokeRelation.collection.insertMany(relations, { ordered: false }).catch(() => { });
}

async function updateRelationsForManualStub(functionId, fromStubName, toStubName) {
  functionId = mongoose.Types.ObjectId(functionId);
  const func = await Function.findOne(functionId);
  await Promise.all([
    functionRelations.updateManualStub(functionId, func.versionType, fromStubName, toStubName),
    InvokeRelation.updateMany(
      {
        versionId: func.versionId,
        fromFuncId: functionId,
        toFuncMangledId: fromStubName,
        toFuncFileId: func.fileId,
      },
      {
        $set: {
          toFuncMangledId: toStubName,
          toFuncName: toStubName,
        },
      }
    ),
  ]);
}

/**
 * 获取被调函数的父函数集合
 * @param {Array<String>} functionIds 函数Id数组
 * @param {String} versionId
 * @returns {Promise}
 */
async function getInvokeFunctions(functionIds, versionId) {
  const funcIdSet = new Set();
  functionIds = functionIds.map((functionId) => mongoose.Types.ObjectId(functionId));
  return Promise.all([
    InvokeRelation.find(
      {
        toFuncId: { $in: functionIds },
      },
      ['functionId']
    ),
    functionRelations.getParentFuncs(functionIds, versionId),
  ])
    .then(([invokeRelations, funcRelations]) => {
      invokeRelations.forEach((relation) => {
        funcIdSet.add(relation.functionId.toString());
      });
      funcRelations.forEach((id) => {
        funcIdSet.add(id.toString());
      });
      return [...funcIdSet];
    });
}
async function bulkUpdateRelations(writes) {
  await InvokeRelation.bulkWrite(writes);
}

/**
 * 根据callStack增加调用关系覆盖情况
 * @param {String} testId 测试用例ID
 * @param {String} versionId 版本ID
 * @param {Array<Object>} callStack
 */
async function invokeEdgeCovered(testId, versionId, callStack) {
  const vid = mongoose.Types.ObjectId(versionId);
  const tid = mongoose.Types.ObjectId(testId);
  const test = await TestCase.findOne({ _id: tid });
  if (!test) {
    return;
  }
  const funcId = test.functionId; // 被测函数
  // eslint-disable-next-line no-use-before-define
  await invokeEdgeUnCovered(tid, funcId); // 先清空之前的覆盖信息
  const edges = new Set();
  const filePaths = new Set();
  const mangledIds = new Set();
  const invokes = [];
  utility.recursiveIterCallstack(callStack, (obj, currentKey, trace) => {
    const [mangledId, fromFilePath] = currentKey.split('@');
    mangledIds.add(mangledId);
    filePaths.add(fromFilePath);
    // 获取所有的调用: `mangled@file->mangled@file`
    Object.keys(obj.call).forEach((keyCalled) => {
      edges.add(`${currentKey}->${keyCalled}`);
    });
    invokes.push({
      trace,
      currentKey,
      obj,
    });
  });

  const files = await File.find({ versionId: vid, path: { $in: [...filePaths] } }, {
    _id: 1,
    path: 1,
  });
  // 文件map，为了通过filePath找到对应的fileId
  const fileMap = files.reduce((map, file) => {
    map.set(file.path, file._id);
    return map;
  }, new Map());
  const filePathMap = files.reduce((map, file) => {
    map.set(file._id.toString(), file.path);
    return map;
  }, new Map());

  const funcs = await Function.find({
    versionId: vid,
    mangledId: { $in: [...mangledIds] },
  }, {
    _id: 1,
    mangledId: 1,
    fileId: 1,
  });
  const funcMap = funcs.reduce((map, func) => {
    map.set(`${func.mangledId}@${filePathMap.get(func.fileId.toString())}`, func);
    return map;
  }, new Map());

  // 更新invoke-relations表
  const query = {
    functionId: funcId,
    $or: [],
  };
  [...edges].forEach((edge) => {
    const [from, to] = edge.split('->');
    const [funcFrom, pathFrom] = from.split('@');
    const [funcTo, pathTo] = to.split('@');
    if (fileMap.get(pathFrom) && fileMap.get(pathTo)) {
      query.$or.push({
        fromFuncMangledId: funcFrom,
        fromFuncFileId: mongoose.Types.ObjectId(fileMap.get(pathFrom)),
        toFuncMangledId: funcTo,
        toFuncFileId: mongoose.Types.ObjectId(fileMap.get(pathTo)),
      });
    }
  });
  if (query.$or.length) {
    await InvokeRelation.updateMany(query, {
      $addToSet: { testCases: tid },
    });
  }

  // 更新test-invoke表
  const docs = [];
  // eslint-disable-next-line no-restricted-syntax
  for (const { trace, currentKey, obj } of invokes) {
    const [fromFuncMangledId, fromFilePath] = currentKey.split('@');
    const fromFuncFileId = fileMap.get(fromFilePath)._id;
    // console.log('currentKey:', currentKey);
    const fromFunc = funcMap.get(currentKey);
    if (fromFunc) {
      const fromFuncId = fromFunc._id;
      const preFuncIds = trace.map((key) => funcMap.get(key)._id).concat(fromFuncId);
      const callFuncs = Object.keys(obj.call).map((key) => {
        const [toFuncMangledId, toFuncFilePath] = key.split('@');
        const toFuncId = funcMap.has(key) ? funcMap.get(key)._id : null;
        const callexprs = Object.keys(obj.call[key].callexpr).map((callexprId) => ({
          callexprId,
          times: obj.call[key].callexpr[callexprId].count,
        }));
        return {
          callexprs,
          toFuncMangledId,
          toFuncFilePath,
          toFuncId,
        };
      });
      if (callFuncs.length) {
        docs.push({
          versionId: vid,
          functionId: funcId,
          testcaseId: tid,
          preFuncIds,
          fromFuncId,
          fromFuncMangledId,
          fromFuncFileId,
          callFuncs,
        });
      }
    }
  }
  if (docs.length > 0) {
    await TestInvoke.collection.insertMany(docs);
  }
  logger.verbose(JSON.stringify(docs, null, 2));
}

/**
 * 删除用例时，删除对应的调用关系边的覆盖
 * @param {string|ObjectId} testId 测试用例ID
 * @param {string|ObjectId} functionId 被测入口函数ID
 */
async function invokeEdgeUnCovered(testId, functionId) {
  const funcId = mongoose.Types.ObjectId(functionId);
  const tid = mongoose.Types.ObjectId(testId);
  await TestInvoke.deleteMany({
    testcaseId: tid,
  });
  await InvokeRelation.updateMany({
    functionId: funcId,
    testCases: tid,
  }, {
    $pull: { testCases: tid },
  });
}
/**
 * 删除入口函数调用关系百边的覆盖
 * @param {string|ObjectId} functionId 入口函数id
 */
async function invokeEdgeUnCoveredByFunctionId(functionId) {
  const fid = mongoose.Types.ObjectId(functionId);
  await TestInvoke.deleteMany({
    functionId: fid,
  });
  return InvokeRelation.updateMany({ functionId: fid }, { $unset: { testCases: 1 } });
}
/**
 * 删除版本调用关系边的覆盖
 * @param {string|ObjectId} versionId 版本id
 */
async function invokeEdgeUnCoveredByVersionId(versionId) {
  const vid = mongoose.Types.ObjectId(versionId);
  await TestInvoke.deleteMany({
    versionId: vid,
  });
  return InvokeRelation.updateMany({ versionId: vid }, { $unset: { testCases: 1 } });
}

async function updateDefaultStubDepth(versionId, depth, originalDepth = defaultStubDepth) {
  const funcs = await Function.find({ versionId: mongoose.Types.ObjectId(versionId), kind: { $in: fileElements.getCountableKinds() } }, ['_id', 'kind']);
  const specialFunctionIds = new Set();
  funcs.forEach((func) => {
    if (func.kind === fileElements.elementKind.specialFunction) {
      specialFunctionIds.add(func._id.toString());
    }
  });
  await utility.arrayChunkOperation(funcs, 10, async (func) => {
    const functionId = func._id;
    const relations = await retrieveRelations(func._id, { completeDepth: true });
    if (relations.length === 0) {
      return Promise.resolve();
    }
    const isDefineFuncs = [];
    // 未定义函数，或定义函数当前选择为调用文件
    const notDefineFuncs = [];
    relations.forEach((func) => {
      const toFuncId = func.toFuncId ? func.toFuncId.toString() : '';
      // 特化函数/手动添加的桩函数不可以改变打桩状态
      if ((toFuncId && specialFunctionIds.has(toFuncId)) || func.toFuncName === func.toFuncMangledId) {
        return;
      }
      depth = depth ? parseInt(depth, 10) : null;
      originalDepth = originalDepth ? parseInt(originalDepth, 10) : null;
      let updated = false;
      if (func.depth.depths.has(originalDepth)) {
        if (func.depth.depths.has(depth)) {
          return;
        }
        if (func.enableStub !== false) {
          updated = true;
          func.enableStub = false;
        }
      } else if (func.depth.depths.has(depth)) {
        if (func.enableStub !== true) {
          updated = true;
          func.enableStub = true;
        }
      }
      if (!updated) {
        return;
      }
      if (toFuncId) {
        isDefineFuncs.push(func);
        return;
      }
      notDefineFuncs.push(func);
    });
    await Promise.all(notDefineFuncs.map((func) => updateStubStatus(
      functionId,
      undefined,
      func.toFuncMangledId,
      func.toFuncName,
      func.enableStub
    )));
    await Promise.all(isDefineFuncs.map((func) => updateStubStatus(
      functionId,
      func.toFuncId,
      undefined,
      undefined,
      func.enableStub
    )));
    return Promise.resolve();
  });
  await functions.updateInvokeCoverageTotal(funcs.map((func) => func._id), versionId);
  return Promise.resolve();
}

eventHub.on(eventKind.EVENT_FUNCTION_REMOVED, ({ functionIds, versionId }) => {
  if (!functionIds) {
    return removeRelationsOfVersions(versionId);
  }
  return removeRelationsOfFunctions(functionIds, versionId);
});
eventHub.on(eventKind.EVENT_FUNCTION_RESET, async ({ functionIds }) => {
  await InvokeRelation.deleteMany({ functionId: { $in: functionIds } });
});

module.exports = {
  defaultStubDepth,
  bulkUpdateRelations,
  createRelations,
  createRelationsForConstructor,
  createRelationsForManualStub,
  filterRelations,
  getInvokeCount,
  getInvokeFunctions,
  updateDefaultStubDepth,
  updateInvokeFile,
  updateStubStatus,
  updateRelationsForManualStub,
  removeRelation,
  retrieveRelations,

  invokeEdgeCovered,
  invokeEdgeUnCovered,
  invokeEdgeUnCoveredByFunctionId,
  invokeEdgeUnCoveredByVersionId,
  stubFuncsByDefaultDepth,
};
exports = Object.assign(exports, module.exports);
